name: CI

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

on:
  pull_request:
    branches:
    - main
  push:
    branches:
    - main
  release:
    types: [published]
  schedule:
  - cron: '37 11 * * 1,2,3,4,5'  # early morning (11:37 UTC / 4:37 AM PDT) Monday - Friday

env:
  # NOTE: Need to update `TORCH_VERSION`, and `TORCH_*_INSTALL` for new torch releases.
  TORCH_VERSION: 1.12.0
  # TORCH_CPU_INSTALL: conda install pytorch torchvision torchaudio cpuonly -c pytorch
  # TORCH_GPU_INSTALL: conda install pytorch torchvision torchaudio cudatoolkit=11.3 -c pytorch
  TORCH_CPU_INSTALL: pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cpu -c constraints.txt
  TORCH_GPU_INSTALL: pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu113 -c constraints.txt
  # Change this to invalidate existing cache.
  CACHE_PREFIX: v11
  # Disable tokenizers parallelism because this doesn't help, and can cause issues in distributed tests.
  TOKENIZERS_PARALLELISM: 'false'
  # Disable multithreading with OMP because this can lead to dead-locks in distributed tests.
  OMP_NUM_THREADS: '1'
  # See https://github.com/pytorch/pytorch/issues/37377#issuecomment-677851112.
  MKL_THREADING_LAYER: 'GNU'
  DEFAULT_PYTHON_VERSION: 3.8
  # For debugging GPU tests.
  CUDA_LAUNCH_BLOCKING: '1'

defaults:
  run:
    shell: bash -l {0}

jobs:
  changelog:
    name: CHANGELOG
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'

    steps:
    - uses: actions/checkout@v3

    - name: Check if source files have changed
      run: |
        git diff --name-only $(git merge-base origin/main HEAD) | grep '^allennlp/.*\.py$' && echo "source_files_changed=true" >> $GITHUB_ENV || echo "source_files_changed=false" >> $GITHUB_ENV

    - name: Check that CHANGELOG has been updated
      if: env.source_files_changed == 'true'
      run: |
        # If this step fails, this means you haven't updated the CHANGELOG.md
        # file with notes on your contribution.
        git diff --name-only $(git merge-base origin/main HEAD) | grep '^CHANGELOG.md$' && echo "Thanks for helping keep our CHANGELOG up-to-date!"

  style:
    name: Style
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Setup Python
      uses: actions/setup-python@v4
      with:
        python-version: ${{ env.DEFAULT_PYTHON_VERSION }}

    - name: Install requirements
      run: |
        grep -E '^black' dev-requirements.txt | xargs pip install

    - name: Debug info
      run: |
        pip freeze

    - name: Run black
      run: |
        black --check .

  checks:
    name: ${{ matrix.task.name }}
    runs-on: ${{ matrix.task.runs_on }}
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        task:
        - name: Lint
          runs_on: ubuntu-latest
          coverage_report: false
          torch_platform: cpu
          run: |
            make flake8
            make typecheck

        - name: CPU Tests
          runs_on: ubuntu-latest
          coverage_report: true
          torch_platform: cpu
          run: make test

        - name: GPU Tests
          runs_on: [self-hosted, GPU, Multi GPU]
          coverage_report: true
          torch_platform: gpu
          run: make gpu-tests

        - name: Model Tests
          runs_on: ubuntu-latest
          coverage_report: true
          torch_platform: cpu
          run: |
            cd allennlp-models
            make test-with-cov COV=allennlp
            mv coverage.xml ../

    steps:
    - uses: actions/checkout@v3

    - uses: conda-incubator/setup-miniconda@v2
      with:
        miniconda-version: "latest"
        python-version: ${{ env.DEFAULT_PYTHON_VERSION }}

    - name: Set build variables
      run: |
        # Get the exact Python version to use in the cache key.
        echo "PYTHON_VERSION=$(python --version)" >> $GITHUB_ENV
        echo "RUNNER_ARCH=$(uname -m)" >> $GITHUB_ENV
        # Use week number in cache key so we can refresh the cache weekly.
        echo "WEEK_NUMBER=$(date +%V)" >> $GITHUB_ENV

    - name: Set build variables (CPU only)
      if: matrix.task.torch_platform == 'cpu'
      run: |
        echo "TORCH_INSTALL=$TORCH_CPU_INSTALL" >> $GITHUB_ENV

    - name: Set build variables (GPU only)
      if: matrix.task.torch_platform == 'gpu'
      run: |
        echo "TORCH_INSTALL=$TORCH_GPU_INSTALL" >> $GITHUB_ENV

    - uses: actions/cache@v3
      id: virtualenv-cache
      with:
        path: .venv
        key: ${{ env.CACHE_PREFIX }}-${{ env.WEEK_NUMBER }}-${{ runner.os }}-${{ env.RUNNER_ARCH }}-${{ env.PYTHON_VERSION }}-${{ matrix.task.torch_platform }}-${{ hashFiles('setup.py') }}-${{ hashFiles('*requirements.txt', 'constraints.txt') }}

    - name: Setup virtual environment (no cache hit)
      if: steps.virtualenv-cache.outputs.cache-hit != 'true'
      run: |
        python${{ env.DEFAULT_PYTHON_VERSION }} -m venv .venv
        source .venv/bin/activate
        make install TORCH_INSTALL="$TORCH_INSTALL"

    - name: Setup virtual environment (cache hit)
      if: steps.virtualenv-cache.outputs.cache-hit == 'true'
      run: |
        source .venv/bin/activate
        pip install --no-deps -e .[all]
        make download-extras

    - name: Pull and install models repo
      if: matrix.task.name == 'Model Tests'
      env:
        ALLENNLP_VERSION_OVERRIDE: ""  # Don't replace the core library.
      run: |
        source .venv/bin/activate
        git clone https://github.com/allenai/allennlp-models.git
        cd allennlp-models
        # git checkout dependabot/pip/torch-gte-1.7.0-and-lt-1.13.0
        pip install -e .[dev,all]

    - name: Debug info
      run: |
        source .venv/bin/activate
        pip freeze

    - name: Ensure torch up-to-date
      run: |
        source .venv/bin/activate
        python scripts/check_torch_version.py

    - name: ${{ matrix.task.name }}
      run: |
        source .venv/bin/activate
        ${{ matrix.task.run }}

    - name: Prepare coverage report
      if: matrix.task.coverage_report
      run: |
        mkdir coverage
        mv coverage.xml coverage/

    - name: Save coverage report
      if: matrix.task.coverage_report
      uses: actions/upload-artifact@v3
      with:
        name: ${{ matrix.task.name }}-coverage
        path: ./coverage

    - name: Clean up
      if: always()
      run: |
        # Could run into issues with the cache if we don't uninstall the editable.
        # See https://github.com/pypa/pip/issues/4537.
        source .venv/bin/activate
        pip uninstall --yes allennlp allennlp-models

  upload_coverage:
    name: Upload Coverage Report
    timeout-minutes: 5
    if: github.repository == 'allenai/allennlp' && (github.event_name == 'push' || github.event_name == 'pull_request')
    runs-on: ubuntu-latest
    needs: [checks]

    steps:
      # Need to checkout code to get the coverage config.
    - uses: actions/checkout@v3

    - name: Download coverage report from CPU tests
      uses: actions/download-artifact@v3
      with:
        name: CPU Tests-coverage
        path: coverage/cpu_tests

    - name: Download coverage report from GPU Tests
      uses: actions/download-artifact@v3
      with:
        name: GPU Tests-coverage
        path: coverage/gpu_tests

    - name: Download coverage report from model tests
      uses: actions/download-artifact@v3
      with:
        name: Model Tests-coverage
        path: coverage/model_tests

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        files: coverage/cpu_tests/coverage.xml,coverage/gpu_tests/coverage.xml,coverage/model_tests/coverage.xml
        # Ignore codecov failures as the codecov server is not
        # very reliable but we don't want to report a failure
        # in the github UI just because the coverage report failed to
        # be published.
        fail_ci_if_error: false

  # Builds package distribution files for PyPI.
  build_package:
    name: Build Package
    timeout-minutes: 18
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - uses: conda-incubator/setup-miniconda@v2
      with:
        miniconda-version: "latest"
        python-version: ${{ env.DEFAULT_PYTHON_VERSION }}

    - name: Set build variables
      run: |
        echo "PYTHON_VERSION=$(python --version)" >> $GITHUB_ENV
        echo "RUNNER_ARCH=$(uname -m)" >> $GITHUB_ENV
        echo "WEEK_NUMBER=$(date +%V)" >> $GITHUB_ENV
        echo "TORCH_INSTALL=$TORCH_CPU_INSTALL" >> $GITHUB_ENV

    - uses: actions/cache@v3
      id: virtualenv-cache
      with:
        path: .venv
        key: ${{ env.CACHE_PREFIX }}-${{ env.WEEK_NUMBER }}-${{ runner.os }}-${{ env.RUNNER_ARCH }}-${{ env.PYTHON_VERSION }}-cpu-${{ hashFiles('setup.py') }}-${{ hashFiles('*requirements.txt', 'constraints.txt') }}

    - name: Setup virtual environment (no cache hit)
      if: steps.virtualenv-cache.outputs.cache-hit != 'true'
      run: |
        python${{ env.DEFAULT_PYTHON_VERSION }} -m venv .venv
        source .venv/bin/activate
        make install TORCH_INSTALL="$TORCH_INSTALL"

    - name: Setup virtual environment (cache hit)
      if: steps.virtualenv-cache.outputs.cache-hit == 'true'
      run: |
        source .venv/bin/activate
        pip install --no-deps -e .[all]
        make download-extras

    - name: Debug info
      run: |
        source .venv/bin/activate
        pip freeze

    - name: Check and set nightly version
      if: github.event_name == 'schedule'
      run: |
        # Verify that current version is ahead of the last release.
        source .venv/bin/activate
        LATEST=$(scripts/get_version.py latest)
        CURRENT=$(scripts/get_version.py current)
        if [ "$CURRENT" == "$LATEST" ]; then
            echo "Current version needs to be ahead of latest release in order to build nightly release";
            exit 1;
        fi
        # This is somewhat bizarre, but you can't set env variables to bash
        # commands in the action workflow - so we have to use this odd way of
        # exporting a variable instead.
        echo "ALLENNLP_VERSION_SUFFIX=dev$(date -u +%Y%m%d)" >> $GITHUB_ENV

    - name: Check version and release tag match
      if: github.event_name == 'release'
      run: |
        # Remove 'refs/tags/' to get the actual tag from the release.
        source .venv/bin/activate
        TAG=${GITHUB_REF#refs/tags/};
        VERSION=$(scripts/get_version.py current)
        if [ "$TAG" != "$VERSION" ]; then
            echo "Bad tag or version. Tag $TAG does not match $VERSION";
            exit 1;
        fi

    - name: Build core package
      run: |
        # Just print out the version for debugging.
        source .venv/bin/activate
        make version
        python setup.py bdist_wheel sdist

    - name: Save core package
      uses: actions/upload-artifact@v3
      with:
        name: core-package
        path: dist

    - name: Clean up
      if: always()
      run: |
        source .venv/bin/activate
        pip uninstall --yes allennlp

  # Tests installing from the distribution files.
  test_package:
    name: Test Package
    timeout-minutes: 10
    needs: [build_package]  # needs the package artifact created from 'build_package' job.
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python: ['3.7', '3.8', '3.9']
        # check that CLI remains working for all package flavors;
        # currently allennlp[checklist]==allennlp[all], so avoiding duplication on this
        flavor: ['', '[all]']

    steps:
    - uses: actions/checkout@v3

    - name: Setup Python
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python }}

    - name: Install requirements
      run: |
        pip install --upgrade pip setuptools wheel

    - name: Download core package
      uses: actions/download-artifact@v3
      with:
        name: core-package
        path: dist

    - name: Install core package
      run: |
        pip install $(ls dist/*.whl)${{ matrix.flavor }} -c constraints.txt

    - name: Download NLTK prerequisites
      run: |
        make download-extras

    - name: Cleanup workspace
      run: |
        rm -rf allennlp/ setup.py tests/ test_fixtures/

    - name: Pip freeze
      run: |
        pip freeze

    - name: Test install
      run: |
        allennlp test-install

  # Builds Docker image from the core distribution files and uploads to Docker Hub.
  docker:
    name: Docker (CUDA ${{ matrix.cuda }})
    timeout-minutes: 30
    if: github.repository == 'allenai/allennlp'
    runs-on: ubuntu-latest
    strategy:
      matrix:
        cuda: ['11.3']

    steps:
    - uses: actions/checkout@v3

    - name: Set image name and torch version
      env:
        CUDA: ${{ matrix.cuda }}
      run: |
        echo "DOCKER_TORCH_VERSION=${TORCH_VERSION}-cuda${CUDA}-python3.8" >> $GITHUB_ENV;
        if [[ $GITHUB_EVENT_NAME == 'release' ]]; then
            echo "DOCKER_IMAGE_NAME=allennlp/allennlp:${GITHUB_REF#refs/tags/}-cuda${CUDA}" >> $GITHUB_ENV;
        else
            echo "DOCKER_IMAGE_NAME=allennlp/commit:${GITHUB_SHA}-cuda${CUDA}" >> $GITHUB_ENV;
        fi

    - name: Build image
      run: |
        make docker-image DOCKER_IMAGE_NAME="$DOCKER_IMAGE_NAME" DOCKER_TORCH_VERSION="$DOCKER_TORCH_VERSION"

    - name: Test image
      run: |
        docker run --rm $DOCKER_IMAGE_NAME test-install

    - name: Authenticate to Docker Hub
      if: github.event_name == 'release' || github.event_name == 'push'
      run: |
        docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}

    - name: Upload image
      if: github.event_name == 'release' || github.event_name == 'push'
      run: |
        docker push $DOCKER_IMAGE_NAME

    - name: Upload default commit image
      # CUDA 11.3 is currently our default.
      if: github.event_name == 'push' && matrix.cuda == '11.3'
      run: |
        docker tag $DOCKER_IMAGE_NAME allennlp/commit:${GITHUB_SHA}
        docker push allennlp/commit:${GITHUB_SHA}

    - name: Upload latest image
      # CUDA 11.3 is currently our default.
      if: github.event_name == 'release' && matrix.cuda == '11.3'
      run: |
        docker tag $DOCKER_IMAGE_NAME allennlp/allennlp:latest
        docker push allennlp/allennlp:latest

  # Builds the API documentation and pushes it to the appropriate folder in the
  # allennlp-docs repo.
  docs:
    name: Docs
    timeout-minutes: 15
    # Don't run for forks.
    if: github.repository == 'allenai/allennlp'
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - name: Setup SSH Client 🔑
      if: github.event_name == 'release' || github.event_name == 'push'
      uses: webfactory/ssh-agent@v0.5.4
      with:
        ssh-private-key: ${{ secrets.DOCS_DEPLOY_KEY }}

    - uses: conda-incubator/setup-miniconda@v2
      with:
        miniconda-version: "latest"
        python-version: ${{ env.DEFAULT_PYTHON_VERSION }}

    - name: Set build variables
      run: |
        echo "PYTHON_VERSION=$(python --version)" >> $GITHUB_ENV
        echo "RUNNER_ARCH=$(uname -m)" >> $GITHUB_ENV
        echo "WEEK_NUMBER=$(date +%V)" >> $GITHUB_ENV
        echo "TORCH_INSTALL=$TORCH_CPU_INSTALL" >> $GITHUB_ENV

    - uses: actions/cache@v3
      id: virtualenv-cache
      with:
        path: .venv
        key: ${{ env.CACHE_PREFIX }}-${{ env.WEEK_NUMBER }}-${{ runner.os }}-${{ env.RUNNER_ARCH }}-${{ env.PYTHON_VERSION }}-cpu-${{ hashFiles('setup.py') }}-${{ hashFiles('*requirements.txt', 'constraints.txt') }}

    - name: Setup virtual environment (no cache hit)
      if: steps.virtualenv-cache.outputs.cache-hit != 'true'
      run: |
        python${{ env.DEFAULT_PYTHON_VERSION }} -m venv .venv
        source .venv/bin/activate
        make install TORCH_INSTALL="$TORCH_INSTALL"

    - name: Setup virtual environment (cache hit)
      if: steps.virtualenv-cache.outputs.cache-hit == 'true'
      run: |
        source .venv/bin/activate
        pip install --no-deps -e .[all]
        make download-extras

    - name: Debug info
      run: |
        source .venv/bin/activate
        pip freeze

    - name: Prepare environment
      run: |
        if [[ $GITHUB_EVENT_NAME == 'release' ]]; then
            echo "DOCS_FOLDER=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV;
            echo "BASE_SOURCE_LINK=https://github.com/allenai/allennlp/blob/${GITHUB_REF#refs/tags/}/allennlp/" >> $GITHUB_ENV;
        else
            echo "DOCS_FOLDER=main" >> $GITHUB_ENV;
            echo "BASE_SOURCE_LINK=https://github.com/allenai/allennlp/blob/main/allennlp/" >> $GITHUB_ENV;
        fi

    - name: Build docs
      run: |
        source .venv/bin/activate
        env PYTHONPATH=. ./scripts/build_docs.sh

    - name: Print the ref
      run: |
        echo ${{ github.ref }}

    - name: Configure Git
      if: github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
      run: |
        git config --global user.email "ai2service@allenai.org"
        git config --global user.name "ai2service"
        git config --global push.default simple

    - name: Stage docs
      if: github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
      run: |
        echo "Staging docs to $DOCS_FOLDER"

        # Checkout allennlp-docs to /allennlp-docs
        git clone git@github.com:allenai/allennlp-docs.git ~/allennlp-docs
        
        # Copy the generated docs to the checked out docs repo
        rm -rf ~/allennlp-docs/$DOCS_FOLDER/
        mkdir -p ~/allennlp-docs/$DOCS_FOLDER
        cp -r site/* ~/allennlp-docs/$DOCS_FOLDER

    - name: Update shortcuts
      if: github.event_name == 'release'
      run: |
        # Fail immediately if any step fails.
        set -e
        source .venv/bin/activate

        LATEST=$(./scripts/get_version.py latest)
        STABLE=$(./scripts/get_version.py stable)

        cd ~/allennlp-docs/

        echo "Updating latest/index.html to point to $LATEST"
        mkdir -p latest
        cat >latest/index.html << EOL
        <!DOCTYPE html>
        <html>
          <head>
            <meta http-equiv="Refresh" content="0; url=/${LATEST}/" />
          </head>
          <body>
            <p>Please follow <a href="/${LATEST}/">this link</a>.</p>
          </body>
        </html>
        EOL

        echo "Updating stable/index.html to point to $STABLE"
        mkdir -p stable
        cat >stable/index.html << EOL
        <!DOCTYPE html>
        <html>
          <head>
            <meta http-equiv="Refresh" content="0; url=/${STABLE}/" />
          </head>
          <body>
            <p>Please follow <a href="/${STABLE}/">this link</a>.</p>
          </body>
        </html>
        EOL

    - name: Deploy docs
      if: github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
      run: |
        # And push them up to GitHub
        cd ~/allennlp-docs/
        git add -A
        git commit -m "automated update of the docs"
        git push

    - name: Re-write docs commit history
      if: github.event_name == 'release'
      run: |
        cd ~/allennlp-docs/
        git checkout --orphan latest_branch
        git add -A
        git commit -m "Re-write commit history"
        git branch -D master  # remove old master branch
        git branch -m master  # rename clean new branch to master branch
        git push -f origin master

    - name: Clean up
      if: always()
      run: |
        source .venv/bin/activate
        pip uninstall --yes allennlp

  # Publish the core distribution files to PyPI.
  publish:
    name: PyPI
    timeout-minutes: 10
    needs: [style, checks, build_package, test_package, docker, docs]
    # Only publish to PyPI on releases and nightly builds to "allenai/allennlp" (not forks).
    if: github.repository == 'allenai/allennlp' && (github.event_name == 'release' || github.event_name == 'schedule')
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Setup Python
      uses: actions/setup-python@v4
      with:
        python-version: ${{ env.DEFAULT_PYTHON_VERSION }}

    - name: Install requirements
      run: |
        pip install --upgrade pip setuptools wheel twine

    - name: Download core package
      uses: actions/download-artifact@v3
      with:
        name: core-package
        path: dist

    - name: Publish core package
      run: |
        twine upload -u allennlp -p ${{ secrets.PYPI_PASSWORD }} dist/*
